Skip to content

fix(cli): add idempotency-key protection to all write tools - #49

Draft
pandres95 wants to merge 1 commit into
mainfrom
fix/idempotency-keys
Draft

pandres95 wants to merge 1 commit into
mainfrom
fix/idempotency-keys

Conversation

@pandres95

Copy link
Copy Markdown
Contributor

Summary

  • Adds packages/cli/src/mcp/idempotency.ts: deterministicIdempotencyKey(operation, params, windowMinutes=5), deriving a stable SHA-256 key from the operation name, its params, and a 5-minute time bucket.
  • Every CLI write tool now accepts an optional idempotencyKey input; when omitted, it falls back to the deterministic key. Compound tools (e.g. create_card, add_spending_category) suffix the key per internal step (:card, :fund) so each underlying write gets its own protected key.
  • Fixed tools: transfer, batch_transfer, create_account (funding step), fund_card, add_spending_category (funding step), create_card, create_disposable_card, create_pse_order, create_bank_transfer_order, create_breb_order, topup_via_pse, cashout_to_bank, send_to_breb_key.

Why

Live testing surfaced a real double-submission bug: a single transfer call resulted in two independently-settled on-chain transactions (double the requested amount moved), traced to a transport-level retry after an MCP reconnect. No CLI write call ever passed an idempotency key to the SDK's HTTP client (packages/core/src/http-client.ts already supports one — it generates a fresh UUID per request() call, but that's only reused across that call's own internal retries, not across two genuinely independent calls). This time it was an inter-account transfer to the same user; in other flows (bank cashouts, BRE-B payouts) an unprotected retry could move real money to a third party twice.

Test plan

  • bun run --filter @bloque/cli typecheck — clean
  • bun test packages/cli — 51 pass, 0 fail (includes new idempotency.test.ts, 5 tests covering stability, param/operation/time-window sensitivity, and digest shape)
  • End-to-end BRE-B send retest (send_to_breb_key to bbva@pablodorado.com) once this ships, to confirm no duplicate settlement under a forced retry

🤖 Generated with Claude Code

A single transfer call was found to produce two independently-settled
on-chain transactions when a transport-level retry occurred, because
no CLI write call ever passed an idempotency key to the SDK's HTTP
client. Thread a deterministic (content + time-window derived) key
through every write tool by default, with an optional override so
callers can supply their own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant